package model;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;

public class DistributionGenerator {
	
	public DistributionGenerator() {
	}
	
	public static ArrayList<FrequencyPair> generateUniform(int lowerBound, int upperBound, int popSize) {
		ArrayList<FrequencyPair> dist = new ArrayList<FrequencyPair>();
		
		int overflow = popSize % (upperBound - lowerBound + 1);
		
		for(int i = 0; i <= upperBound - lowerBound; i++) {
			dist.add(new FrequencyPair(i + lowerBound));
			dist.get(i).setFrequency(BigDecimal.valueOf(Math.floor(popSize / (upperBound - lowerBound + 1))));
			if (i < overflow)
				dist.get(i).addOne();
		}
		
		return dist;
	}
	
	public static ArrayList<FrequencyPair> generateRandom(int lowerBound, int upperBound, int popSize) {
		ArrayList<FrequencyPair> dist = new ArrayList<FrequencyPair>();
		
		for(int i = 0; i <= upperBound - lowerBound; i++)
			dist.add(new FrequencyPair(i + lowerBound));
		
		Random rng = new Random();
		for (int i = 0; i < popSize; i++) {
			dist.get(rng.nextInt(upperBound-lowerBound + 1)).addOne();
		}
		
		return dist;
	}
	
	public static ArrayList<FrequencyPair> generateNormal(int lowerBound, int upperBound, int popSize) {
		ArrayList<FrequencyPair> dist = new ArrayList<FrequencyPair>();
		BigDecimal factor = BigDecimal.valueOf(2).pow(upperBound - lowerBound), maxVal = BigDecimal.ZERO;
		BigDecimal currSize = BigDecimal.ZERO;
		int range = upperBound - lowerBound, modeCount = 0;
		
		for(int i = 0; i <= upperBound - lowerBound; i++) {
			dist.add(new FrequencyPair(i + lowerBound));
			
			BigDecimal freq = choose(upperBound - lowerBound, i).multiply(BigDecimal.valueOf(popSize)).divide(factor, MathContext.DECIMAL128);
			dist.get(i).setFrequency(freq);
			currSize = currSize.add(dist.get(i).getFrequency());
		}

		for (int i = 0; i <= range; i++) {
			if (dist.get(i).getFrequency().compareTo(maxVal) > 0) {
				modeCount = 1;
				maxVal = dist.get(i).getFrequency();
			}
			else if (dist.get(i).getFrequency() == maxVal) {
				modeCount++;
			}
		}
		
		BigDecimal toAdd = BigDecimal.valueOf(popSize).subtract(currSize).divide(BigDecimal.valueOf(modeCount), MathContext.DECIMAL128);
		
		if (toAdd.compareTo(BigDecimal.ZERO) > 0) {
			for (int i = 0; i <= range; i++) {
				if (dist.get(i).getFrequency() == maxVal) {
					dist.get(i).addCount(toAdd);
					currSize = currSize.add(toAdd);
				}
			}
		}
		
		return dist;
	}
	
	public static ArrayList<FrequencyPair> generateBimodal(int lowerBound, int upperBound, int popSize) {
		ArrayList<FrequencyPair> dist = new ArrayList<FrequencyPair>();
		int range = upperBound - lowerBound;
		BigDecimal factor = BigDecimal.valueOf(2).pow(range/2);
		BigDecimal currSize = BigDecimal.ZERO;
		BigDecimal maxVal = BigDecimal.ZERO;
		int modeCount = 0;
		
		if (range >= 4) {
			for(int i = 0; i <= range; i++) {
				dist.add(new FrequencyPair(i + lowerBound));
				
				BigDecimal ch = choose(range/2, i).multiply(choose(range/2, i - range/2 - 1 + ((range%2==0)? 1:0)));
				
				BigDecimal freq = BigDecimal.valueOf(popSize).divide(factor, MathContext.DECIMAL128).multiply(ch).divide(BigDecimal.valueOf(2), MathContext.DECIMAL128);
				
				currSize = currSize.add(freq);
				dist.get(i).setFrequency(freq);
			}
			
			
			for (int i = 0; i <= range; i++) {
				if (dist.get(i).getFrequency().compareTo(maxVal) > 0) {
					modeCount = 1;
					maxVal = dist.get(i).getFrequency();
				}
				else if (dist.get(i).getFrequency().equals(maxVal)) {
					modeCount++;
				}
			}
			
			BigDecimal toAdd = BigDecimal.valueOf(popSize).subtract(currSize).divide(BigDecimal.valueOf(modeCount), MathContext.DECIMAL128);
			
			for (int i = 0; i <= range; i++) {
				if (dist.get(i).getFrequency().equals(maxVal)) {
					dist.get(i).addCount(toAdd);
					currSize = currSize.add(toAdd);
				}
			}
			
			return dist;
		}

		// range < 4, modes = sides
		for(int i = 0; i <= range; i++) {
			dist.add(new FrequencyPair(i + lowerBound));
			
			factor = BigDecimal.valueOf(2).pow(range);
			
			BigDecimal ch = choose(range, (int)Math.floor(range/2.0)-i).multiply(choose(range, i+(int)Math.floor(range/2.0)-range));
			BigDecimal freq = BigDecimal.valueOf(popSize).divide(factor, MathContext.DECIMAL128).multiply(ch);
			
			currSize = currSize.add(freq);
			dist.get(i).setFrequency(freq);
		}
		
		for (int i = 0; i <= range; i++) {
			if (dist.get(i).getFrequency().compareTo(maxVal) > 0) {
				modeCount = 1;
				maxVal = dist.get(i).getFrequency();
			}
			else if (dist.get(i).getFrequency().equals(maxVal)) {
				modeCount++;
			}
		}
		
		
		BigDecimal toAdd = BigDecimal.valueOf(popSize).subtract(currSize).divide(BigDecimal.valueOf(modeCount), MathContext.DECIMAL128);
		
		for (int i = 0; i <= range; i++) {
			if (dist.get(i).getFrequency().equals(maxVal)) {
				dist.get(i).addCount(toAdd);
				currSize = currSize.add(toAdd);
			}
		}
		
		return dist;
	}
	
	public static ArrayList<FrequencyPair> generateSkewed(int lowerBound, int upperBound, int popSize, boolean isSkewedLeft){
		ArrayList<FrequencyPair> normalDist = generateNormal(lowerBound, upperBound, popSize);
		ArrayList<FrequencyPair> skewedDist = new ArrayList<FrequencyPair>();
		int peakIndex = 0;
		
		if(normalDist.size() == 3){
			
			for(FrequencyPair fp: normalDist)
				skewedDist.add(fp.getCopy());
			
			if(isSkewedLeft){
				BigDecimal temp = normalDist.get(0).getFrequency();
				skewedDist.get(0).setFrequency(normalDist.get(2).getFrequency());
				skewedDist.get(1).setFrequency(temp);
				skewedDist.get(2).setFrequency(normalDist.get(1).getFrequency());
			}
			else{
				BigDecimal temp = normalDist.get(0).getFrequency();
				skewedDist.get(0).setFrequency(normalDist.get(1).getFrequency());
				skewedDist.get(1).setFrequency(normalDist.get(2).getFrequency());
				skewedDist.get(2).setFrequency(temp);				
			}
		}
		else{
			if(isSkewedLeft){
				peakIndex = 2*normalDist.size()/3; 
				
				if(peakIndex >= normalDist.size())
					peakIndex = normalDist.size() - 1;
				
				//shift to the right
				for(int i = peakIndex; i >= 0; i--)
					skewedDist.add(0, normalDist.get(i).getCopy());
				
				for(int i= peakIndex + 1; i < normalDist.size(); i++)
					skewedDist.add(0, normalDist.get(i).getCopy());
				
				//gather all the frequencies to the right of the peak, and sort them
				ArrayList<BigDecimal> toSort = new ArrayList<BigDecimal>();
				
				for(int i = 0; i < peakIndex; i++)
					toSort.add(skewedDist.get(i).getFrequency());
				
				Collections.sort(toSort);
				
				//overwrite the frequencies
				for(int i = 0; i < peakIndex; i++)
					skewedDist.get(i).setFrequency(toSort.get(i));
			
				if(skewedDist.size() == 2){
					FrequencyPair first = skewedDist.get(0);
					FrequencyPair second = skewedDist.get(1);
					if(first.getFrequency() == second.getFrequency())
					{
						BigDecimal temp = first.getFrequency();
						first.setFrequency(temp.subtract(BigDecimal.ONE));
						second.setFrequency(temp.add(BigDecimal.ONE));
					}
				}
			}
			else{
				peakIndex = normalDist.size()/3;
				//shift to the left
				for(int i = peakIndex, count = 0; i < normalDist.size(); i++, count++)
					skewedDist.add(normalDist.get(i).getCopy());
				
				
				for(int i=0; i < peakIndex; i++)
					skewedDist.add(normalDist.get(i).getCopy());
				
				//gather all the frequencies to the right of the peak, and sort them
				ArrayList<BigDecimal> toSort = new ArrayList<BigDecimal>();
				
				for(int i = peakIndex + 1; i < skewedDist.size(); i++)
					toSort.add(skewedDist.get(i).getFrequency());
				
				Collections.sort(toSort);
				
				//overwrite the frequencies
				for(int i = peakIndex + 1, j=toSort.size()-1; i<skewedDist.size() && j>=0; i++,j--)
					skewedDist.get(i).setFrequency(toSort.get(j));
			
				if(skewedDist.size() == 2){
					FrequencyPair first = skewedDist.get(0);
					FrequencyPair second = skewedDist.get(1);
					if(first.getFrequency() == second.getFrequency())
					{
						BigDecimal temp = first.getFrequency();
						first.setFrequency(temp.add(BigDecimal.ONE));
						second.setFrequency(temp.subtract(BigDecimal.ONE));
					}
				}
			
			}	
		}
		
		//set the num/x-value of the distribution to match lowerbound - upperbound(because it gets rotated around in the algo)
		for(int i=lowerBound, j=0; i<=upperBound && j < skewedDist.size(); i++, j++)
			skewedDist.get(j).setNum(i);
		
		//do necessary adjustments since it is expected that some deviation may occur due to rounding error
		BigDecimal deviation = BigDecimal.ZERO;
		for(FrequencyPair fp: skewedDist)
			deviation = deviation.add(fp.getFrequency());
		
		deviation = deviation.subtract(BigDecimal.valueOf(popSize));
		
		if(deviation.signum() == 1)
			skewedDist.get(peakIndex).addCount(deviation);
		else if(deviation.signum() == -1)
			skewedDist.get(peakIndex).addCount(deviation.negate());
		
		
		return skewedDist;
	}
	
	public static BigDecimal choose(int n, int k) {
		BigDecimal ans = BigDecimal.ONE;
		
		if (k > n || k < 0)
			return BigDecimal.ONE;
		
		if (n - k < k)
			k = n - k;
		
		for (int i = 1; i <= k; i++) {
			ans = ans.multiply(BigDecimal.valueOf(n-i+1));
		}
		
		for (int i = 1; i <= k; i++) {
			ans = ans.divide(BigDecimal.valueOf(i), MathContext.DECIMAL128);
		}
		
		return ans;
	}
	
	public static void main(String args[]) {
		for (int i = 90; i <= 100; i++) {
			System.out.println(DistributionGenerator.generateNormal(10, i, 100));
		}
	}
}